package ltd.hello.trik.snake;

import lombok.Getter;
import processing.core.PApplet;
import processing.core.PVector;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;

import static processing.core.PApplet.abs;
import static processing.core.PApplet.floor;

/**
 * +----------------------------------------------------------------------
 * |
 * +----------------------------------------------------------------------
 * | <a href="https://www.hellotrik.ltd">Copyright (c) 2024~2025 </a>.
 * +----------------------------------------------------------------------
 * | Licensed MIT
 * +----------------------------------------------------------------------
 * | 作者: 🦠 matrixg 👾 <hellotrik@foxmail.com>
 * | 日期: 2024/9/6
 * +----------------------------------------------------------------------
 */
public class Snake extends SnakeCommon {

    int score = 1;
    int lifeLeft = 200;  //amount of moves the snake can make before it dies
    int lifetime = 0;  //amount of time the snake has been alive
    PVector pVel = new PVector();
    int foodItterate = 0;  //itterator to run through the foodlist (used for replay)

    @Getter
    float fitness = 0;
    @Getter
    boolean dead = false;

    public boolean isLive() {
        return !dead;
    }

    boolean replay = false;  //if this snake is a replay of best snake

    float[] vision;  //snakes vision
    float[] decision;  //snakes decision

    Head head;

    List<Cell> body;  //snakes body
    List<Food> foodList;  //list of food positions (used to replay the best snake)

    Food food;
    NeuralNet brain;

    Snake(PApplet applet) {
        this(hidden_layers, applet);
    }

    Snake(int layers, PApplet applet) {
        super(applet);
        head = Head.builder().applet(applet).build();
        food = new Food(applet);
        body = new ArrayList<>();
        vision = new float[dir.length * 3];
        decision = new float[6];
        foodList = new ArrayList<>();
        foodList.add(food.clone());
        brain = new NeuralNet(dir.length * 3, hidden_nodes, 6, layers, applet);
        body.add(Cell.builder().applet(applet).pos(new PVector(0, SIZE)).build());
        body.add(Cell.builder().applet(applet).pos(new PVector(0, (2 * SIZE))).build());
        score += 2;
    }

    Snake(List<Food> foods, PApplet applet) {
        //this constructor passes in a list of food positions so that a replay can replay the best snake
        super(applet);
        replay = true;
        vision = new float[dir.length * 3];
        decision = new float[6];
        body = new ArrayList<>();
        foodList = foods.stream().map(Food::clone).toList();
        food = foodList.get(foodItterate);
        foodItterate++;
        head = Head.builder().applet(applet).build();
        body.add(Cell.builder().applet(applet).pos(new PVector(0, SIZE)).build());
        body.add(Cell.builder().applet(applet).pos(new PVector(0, (2 * SIZE))).build());
        score += 2;
    }

    boolean bodyCollide(PVector vector) {  //check if a position collides with the snakes body
        return body.stream().anyMatch(cell -> cell.collide(vector));
    }


    boolean wallCollide(PVector vector) {  //check if a position collides with the wall
        return abs(vector.x) >= (applet.height - (5 * SIZE))>>>1 || abs(vector.y) >= (applet.height - (5 * SIZE))>>>1 || abs(vector.z) >= (applet.height - (5 * SIZE))>>>1;
    }

    void show() {  //show the snake
        food.show();
        body.forEach(Cell::show);
        head.show(dead);
    }

    void move() {  //move the snake
        if (!dead) {
            lifetime++;
            lifeLeft--;
            if (food.collide(head)) {
                eat();
            }
            shiftBody();
            if (wallCollide(head.getPos())) {
                dead = true;
            } else if (bodyCollide(head.getPos())) {
                dead = true;
            } else if (lifeLeft <= 0) {
                dead = true;
            }
        }
    }

    void eat() {  //eat food
        int len = body.size() - 1;
        score++;
        if (lifeLeft < 500) {
            if (lifeLeft > 400) {
                lifeLeft = 500;
            } else {
                lifeLeft += 100;
            }
        }
        if (len >= 0) {
            body.add(body.get(len).copy());
        } else {
            body.add(head.copy());
        }
        if (!replay) {
            do {
                food = new Food(applet);
            } while (bodyCollide(food.pos));
            foodList.add(food);
        } else {  //if the snake is a replay, then we dont want to create new random foods, we want to see the positions the best snake had to collect
            food = foodList.get(foodItterate);
            foodItterate++;
        }
    }

    void shiftBody() {  //shift the body to follow the head
        var tempx = head.getPos().copy();
        head.getPos().add(pVel);
        PVector temp2x;
        for (Cell cell : body) {
            temp2x = cell.getPos().copy();
            cell.getPos().set(tempx);
            tempx = temp2x;
        }
    }

    Snake cloneForReplay() {  //clone a version of the snake that will be used for a replay
        Snake clone = new Snake(foodList, applet);
        clone.brain = brain.clone();
        return clone;
    }


    public Snake clone() {  //clone the snake
        Snake clone = new Snake(hidden_layers, applet);
        clone.brain = brain.clone();
        return clone;
    }

    Snake crossover(Snake parent) {  //crossover the snake with another snake
        Snake child = new Snake(hidden_layers, applet);
        child.brain = brain.crossover(parent.brain);
        return child;
    }

    void mutate() {  //mutate the snakes brain
        brain.mutate(mutationRate);
    }

    void calculateFitness() {  //calculate the fitness of the snake
        if (score < 10) {
            fitness = floor(lifetime * lifetime) * PApplet.pow(2, score);
        } else {
            fitness = floor(lifetime * lifetime);
            fitness *= PApplet.pow(2, 10);
            fitness *= (score - 9);
        }
    }


    void look() {  //look in all 8 directions and check for food, body and wall
        vision = new float[dir.length * 3];
        IntStream.range(0, dir.length).parallel().forEach(i -> {
            var integers = dir[i];
            float[] temp = lookInDirection(new PVector(integers[0] * SIZE, integers[1] * SIZE, integers[2] * SIZE));
            vision[i * 3] = temp[0];
            vision[i * 3 + 1] = temp[1];
            vision[i * 3 + 2] = temp[2];
        });
    }

    float[] lookInDirection(PVector direction) {  //look in a direction and check for food, body and wall
        var pos = head.getPos().copy();
        pos.add(direction);
        return new float[]{wallCollide(pos)?1:0, food.collide(pos)?1:0, bodyCollide(pos)?1:0};
    }

    void think() {  //think about what direction to move
        decision = brain.output(vision);
        int maxIndex = 0;
        float max = 0;
        for (int i = 0; i < decision.length; i++) {
            if (decision[i] > max) {
                max = decision[i];
                maxIndex = i;
            }
        }

        switch (maxIndex) {
            case 0:
                moveUp();
                break;
            case 1:
                moveDown();
                break;
            case 2:
                moveLeft();
                break;
            case 3:
                moveRight();
                break;
            case 4:
                moveF();
                break;
            case 5:
                moveB();
                break;
        }
    }

    private void moveB() {
        if (pVel.z != SIZE) {
            pVel.set(0, 0, -SIZE);
        }
    }

    private void moveF() {
        if (pVel.z != -SIZE) {
            pVel.set(0, 0, SIZE);
        }
    }

    void moveUp() {
        if (pVel.y != SIZE) {
            pVel.set(0, -SIZE, 0);
        }
    }

    void moveDown() {
        if (pVel.y != -SIZE) {
            pVel.set(0, SIZE, 0);
        }
    }

    void moveLeft() {
        if (pVel.x != SIZE) {
            pVel.set(-SIZE, 0, 0);
        }
    }

    void moveRight() {
        if (pVel.x != -SIZE) {
            pVel.set(SIZE, 0, 0);
        }
    }
}
